<?
class model extends gravy{

	private $__data = array();
	
	private $__tableName;
	
	private $__needsSave = false;
	
	public $loadRelated = false;
	
	public $recurseRelated = true;
	
	public $loadChildren = array();
	
	public $modelName="model";

	/**
		@TODO - If uniqueCondition is a string, onSave if its an INSERT, check based on the condition whether there are no matching records.
	*/
	public $uniqueCondition=NULL;
	
	public $connection="default";
	
	public $relatedModels=array();
	
	public $columns=array();

	public $uploadsPath = null;
	private $__schema = false;

	public $page = 1;
	public $pages = 1;
	public $rowCount = 0;
	public $recordsPerPage = 0;
	
	public $validationRules = array();

	public function __construct($modelName="model",$id=NULL){
		// call the construct
		parent::__construct();
		
		// if none is specified, just use the default file upload path
		$this->uploadsPath = ( $this->uploadsPath == null ) ? $this->config->uploadsPath : $this->uploadsPath;
		
		// by default the table name for the models are the model name themselves
		$this->modelName=$modelName;
		
		// by default the table name for the models are the model name themselves
		$this->__tableName = $this->modelName;
		
		#Create the database object.
		$this->db=new database($this);

		// If they supplied an ID in the constructor, load the model's data.
		if($id!==NULL){
			$this->getModelFromId($id);
		}
	}

	/*
	** Set's the schema for the model (database).
	**
	** @return void
	*/
	public function setSchema($schema=NULL){
		$has_id=$has_status=$has_dateAdded=$has_dateUpdated=false;
		#$this->debug("<h1>Setting Schema for ".$this->modelName."</h1>",$schema);
		if(is_array($schema) && count($schema)>0 && $schema!==NULL){
			foreach($schema as $key=>$val){
				if($key=="id"){
					$has_id=true;
				}elseif($key=="status"){
					$has_status=true;
				}elseif($key=="dateAdded"){
					$has_dateAdded=true;
				}elseif($key=="dateUpdated"){
					$has_dateUpdated=true;
				}
			}
			$this->__schema=$schema;
			if(!$has_id){
				$this->__schema["id"]=array("type"=>"int");
			}
			if(!$has_status){
				$this->__schema["status"]=array("type"=>"tinyint");
			}
			if(!$has_dateAdded){
				$this->__schema["dateAdded"]=array("type"=>"bigint");
			}
			if(!$has_dateUpdated){
				$this->__schema["dateUpdated"]=array("type"=>"bigint");
			}
		}else{
			$this->debug("<h1>Schema for ".$this->modelName." is bad...</h1>",$schema);
			$this->__schema=false;
		}
	}
	
	/*
	** Is Variable Valid Database ID
	** Checks whether the variable is in a valid format for an ID of a row in the database.
	**
	** @return boolean
	*/
	static function isValidId($id=NULL){
		$result=false;
		if(is_numeric($id)&&(int)$id>0&&$id!==NULL){
			$result=(int)$id;
		}
		return($result);
	}
	
	/*
	** Singleton Accessor
	** Ensures there's only ever one instance of Application running;
	**
	** @return Application
	*/
	static function instance(){
		$result=new Model;
		return(new Model);
	}
	
	
	#Getter Setters
	
	/*
	** Value Setter
	** Sets a model value to be used later
	**
	** @return void
	*/
	public function set($key,$value){
		if($key=="status"){
			// todo - remember to reverse this typecast
			$this->__data[$key]=(bool)$value;
		}else{
			$this->__data[$key]=$value;
		}
		$this->hasChanges(true);
	}

	/*
	** Save Status
	** a little flag on wether the model has changes that haven't been saved
	**
	** @return boolean
	*/
	public function hasChanges($areChanges=false){
		$this->__needsSave=$areChanges;
	}
	
	public function needsSaving(){
		return($this->__needsSave);
	}

	
	/*
	** Value getter
	** returns the value stored locally
	**
	** @return Object
	*/
	public function get($key){
		return((isset($this->__data[$key]))?$this->__data[$key]:false);
	}
	
	/*
	** Get All Data
	** returns all the values stored locally
	**
	** @return Array
	**/
	public function getAllData(){
		return($this->__data);
	}
	
	/*
	** Clear All Data
	** Clears all the values stored locally
	**
	** @return true
	**/
	public function clear(){
		$this->__data=array();
		return(true);
	}
	
	/*
	** Set All Data
	** Sets all the data in the model
	**
	** @return void
	**/
	public function setAllData($ar=NULL){
		if($ar===NULL){
			$this->__data=array();
		}else{
			foreach($ar as $key=>$val){
				if(!is_numeric($key)){
					$this->__data[$key]=$val;
				}
			}
		}
	}
	
	/*
	** Get Template Data
	** returns all the values stored locally for use within the template
	**
	** @return Array
	**/
	public function getTemplateData(){
		return($this->__data);
	}
	
	/*
	** Accepts a file and saves the path to the key value pair
	**
	** @return boolean
	*/
	public function saveUploadedFile($file,$key){
		$result=false;
		if(strlen($file["name"])>0){
			$fileType=split("\.",$file['name']);
			$fileType=array_shift(array_reverse($fileType));
		
			$fileName=md5(basename($file['name']).(string)strtotime("now"));
		
			$path=$this->uploadsPath.$fileName.".".strtolower($fileType);
		
			if(move_uploaded_file($file['tmp_name'],$path)){
				// save the path
				$this->debug("FILE_DEBUG","Ok moved uploaded file.");
				$this->set($key,$path);
				$result=true;
			}else{
				$this->debug("Debug",$file);
				$this->debug("FILE_DEBUG","BA! Can't move file from ".$file['tmp_name']." to ".$path.".");
			}
		}else{
			$this->debug("FILE_DEBUG","Hmmm, file name is zero!");
		}
		return($result);
	}


	#Model Creates, Updates & Deletes

	/*
	** Creates (and returns) a new model
	**
	** @return Model
	**/
	public function create($data){
		$this->set("status",1);
		foreach($data as $key=>$value){
			if(is_string($key)){
				$this->set($key,$value);
			}
		}

		$this->hasChanges(true);

		return($this);
	}

	/*
	** Populates this model with the supplied array's data
	**
	** @return bool
	**/
	public function populate($data){
		foreach($data as $key=>$value){
			if(is_string($key)){
				$this->set($key,$value);
			}
		}
		return(true);
	}

	/*
	** Saves the changes to the model
	** Executes either an INSERT or UPDATE depending on the existance of the ID
	** 
	** @return bool
	**
	*/
	public function save(){
		$result=false;
		
		$this->debug("save() Called on: ".$this->modelName,"");
		// are there changes to save?
		if($this->needsSaving()){
			$this->debug("Needs Saving","");
			// is this an update or a create?
			if($this->get("id")){
				$this->debug("Already has ID","");
				// create a database connection
				$this->dbConnect($this->connection);
				
				// get all the models data
				$data=$this->getAllData();
				
				$dateUpdated=time();
	
				// prepare the query
				$sQl="UPDATE `".$this->modelName."` SET ";
				
				// cycle through the data and create the query string accordingly
				foreach($data as $key=>$value){
					if(strtolower($key)!="id"&&strtolower($key)!="dateupdated"&&strtolower($key)!="dateadded"){
						if($this->__schema){
							$this->debug("<h1>".$this->modelName."'s Schema</h1>",$__schema);
						}
						if(!$this->__schema||array_key_exists($key, $this->__schema)){
							$sQl.="`".$key."`=".$this->db->escape($value).",";
						}else{
							$this->debug("Skipped columns $key as it's not in the schema",$value);
						}
					}
				}
				
				$sQl.="dateUpdated=$dateUpdated";
				$sQl.=" WHERE id='".$this->get("id")."'";
				
				// check the result
				#$this->debug("sQl:$sQl");
				if($this->db->exec($sQl)){
					$result=true;

					$this->set("dateUpdated",$dateUpdated);

					// update the changes flag
					$this->hasChanges(false);
				}else{
					// report on the error
					$this->addError($this->ERROR_MODEL_UPDATE,"model.save.update.dbError",array("sQl"=>$sQl));
				}

				// disconnect from the DB
				$this->dbClose();
			}else{
				$this->debug("Doesnt Have ID","");
				// create
				// connect to the DB
				$this->dbConnect($this->connection);
				
				$dateUpdated=time();
				$dateAdded=time();
				if(!array_key_exists("dateAdded",$this->__data)){
					$this->set("dateAdded",time());
				}
				if(!array_key_exists("dateUpdated",$this->__data)){
					$this->set("dateUpdated",time());
				}
				
				// get all the models data
				$data=$this->getAllData();
				
				// get the models keys and prepare it for an insert statement
				$keys=array_keys($data);
				$values=array_values($data);
				
				// join the keys, so they can be used in the insert
				//$keys="`".join("`,`",$keys)."`";
				
				// cycle through the values and ensure they're quoted accordingly
				//foreach($values as &$value){
				//	$value=$this->db->escape($value);
				//}

				// join the values and prepare the string
				//$values="".join(", ",$values);
				if($this->__schema){
					$this->debug("<h1>".$this->modelName."'s Schema</h1>",$this->__schema);
				}else{
					$this->debug("<h1>".$this->modelName." doesn't have a schema</h1>",$this->__schema);
				}

				$str_keys="";
				$str_values="";
				for($c=0;$c<count($keys);$c++){
					$key=$keys[$c];
					$value=$values[$c];
					if(!$this->__schema||array_key_exists($key, $this->__schema)){
						$str_keys.=",`$key`";
						$str_values.=",".$this->db->escape($value);
					}else{
						$this->debug("Skipped columns $key as it's not in the schema",$value);
					}
				}
				if(strlen($str_keys)>0){
					$str_keys=substr($str_keys,1);
				}
				if(strlen($str_values)>0){
					$str_values=substr($str_values,1);
				}
				
				// prepare the query
				$sQl="INSERT INTO `".$this->modelName."` (".$str_keys.") VALUES (".$str_values.")";
				
				$this->debug("Saving Data",$sQl);
				
				if ($this->db->exec($sQl)){
					// get the inserted ID
					$this->set("id",$this->db->insertId);
					$this->set("dateAdded",$dateUpdated);
					$this->set("dateUpdated",$dateUpdated);
					$this->hasChanges(false);
					$result=true;
				}else{
					// report on the error
					$this->addError($this->ERROR_MODEL_INSERT,"model.save.insert.dbError",array("sQl"=>$sQl));
				}
				
				// disconnect from the DB
				$this->dbClose();
			}
		}else{
			// no changes, log something in the debug
			$this->debug("Model->".$this->modelName,"There are no changes to save");
		}
		return($result);
	}
	
	/*
	** Deletes the entry
	** Nothing is ever deleted in this system, it's just disabled
	** 
	** @return bool
	*/
	public function delete(){
		$this->set("status",false);
		if($this->get("id")){
			return($this->save());
		}else{
			return(false);
		}
	}

	/*
	** Undeletes the entry
	** Nothing is ever deleted in this system, it's just disabled
	** 
	** @return bool
	*/
	public function undelete(){
		$this->set("status",true);
		return($this->save());
	}


	#Model Reads
	
	/*
	** Returns a model with the data supplied
	**
	** @return Model
	*/
	public function getModelFromId($id=0,$extra=NULL,$alternate_id=NULL){
		$result=false;
		$this->dbConnect($this->connection);
		$col=($alternate_id===NULL)?"id":$alternate_id;
		if($alternate_id===NULL){
			$col="id";
			$id=intval($id);
		}else{
			$col=$alternate_id;
			$id="'".$id."'";
		}
		#$where=($status===NULL)?"AND status=1":"AND status=$status";
		$where="";
		if($extra!==NULL&&strlen($extra)>0){
			if(strtoupper(substr(trim($extra),0,4))=="AND "){
				$where=trim($extra);
			}else{
				$where="AND ".trim($extra);
			}
		}
		$sQl="
			SELECT
				*
			FROM
				`".$this->modelName."`
			WHERE
				$col=".$id."
				$where
			ORDER BY
				dateUpdated DESC
			LIMIT
				0,1
		";
		#$this->debug($this->modelName."->getModelFromId()",$sQl);
		//true=saveResultArray.
		if($this->db->exec($sQl)){
			$row=$this->db->row;
			$keys=array_keys($row);
			foreach($keys as $key){
				if(is_string($key)){
					$this->set($key,stripslashes($row[$key]));
				}
			}
			if($this->loadRelated){
				$this->findRelated($this);
			}
			$result=true;
		}
		$this->dbClose();
		return($result);
	}
	
	/*
	** Finds all related models to the current model.
	**
	** @return Model
	*/
	public function findRelated(&$obj,$id=NULL){
		$result=false;
		$id=($id===NULL)?$obj->get("id"):$id;
		if((int)$id>0){
			for($c=0;$c<count($obj->relatedModels);$c++){
				$tmp=new $obj->relatedModels[$c];
				$tmp->loadRelated=$obj->loadRelated;
			}
		}
		return($result);
	}
	
	/*
	** Update a group of records for this model.
	**
	** @return int (number of rows updated) or false if there was an error.
	*/
	public function bulkUpdate($update=NULL,$criteria=NULL){
		$result=false;
		if(strlen($update)>0){
			$this->dbConnect($this->connection);
			//Handle WHERE criterias
			$where=($criteria===NULL)?"":"WHERE $criteria";
			
			$sQl="
				UPDATE
					".$this->modelName."
				SET
					$update
				$where
			";
			//true=saveResultArray.
			if($this->db->exec($sQl)){
				$result=$this->db->affectedRows;
			}else{
				$this->mergeErrors($this->db);
			}
			$this->dbClose();
		}
		return($result);
	}
	
	/*
	** Insert a group of records into this model's table.
	**
	** @return int (number of rows inserted) or false if there was an error.
	*/
	public function bulkInsert($array_columns=NULL,$array_data=NULL,$const_data=NULL){
		$result=false;
		
		$runQuery=false;
		
		$is_const_array=false;
		
		#Generate the Insert Columns
		$sQl="INSERT INTO ".$this->modelName." ($array_columns";
		if(is_array($const_data)){
			$c=0;
			$is_const_array=true;
			foreach($const_data as $key=>$val){
				if(!is_numeric($key)){
					if($c==0&&strlen($array_columns)<1){
						$sQl.="$key";
					}else{
						$sQl.=",$key";
					}
				}
			}
		}
		$sQl.=") VALUES";
		
		if(is_array($array_data)&&count($array_data)>0){
			$runQuery=true;
			for($c=0;$c<count($array_data);$c++){
				if(strlen($array_data[$c])>0){
					$sQl.="('".$array_data[$c]."'";
					if($is_const_array){
						foreach($const_data as $key=>$val){
							if(!is_numeric($key)){
								$sQl.=",$val";
							}
						}
					}
					$sQl.="),";
				}
			}
			$sQl=substr($sQl,0,strlen($sQl)-1);
		}
		
		#$this->debug("sQl",$sQl);
		
		if($runQuery){
			$this->dbConnect($this->connection);
			//true=saveResultArray.
			if($this->db->exec($sQl)){
				$result=$this->db->affectedRows;
			}else{
				$this->mergeErrors($this->db);
			}
			$this->dbClose();
		}
		
		#$this->debug("result",$result);

		return($result);
	}
	
	/*
	** Returns a Count of Rows found by a given criteria. -1 If an error was found.
	**
	** @return int
	*/
	public function count($criteria=NULL){
		$result=-1;
		$this->dbConnect($this->connection);
		
		//Handle WHERE criterias
		$where=($criteria===NULL)?"":"WHERE $criteria";
		
		$sQl="
			SELECT
				COUNT(*) as rowCount
			FROM
				`".$this->modelName."`
			$where
		";
		//true=saveResultArray.
		if($this->db->exec($sQl)){
			$result=$this->db->row["rowCount"];
		}else{
			$this->mergeErrors($this->db);
		}
		$this->dbClose();
		return($result);
	}
	
	/*
	** Returns an array of models based on the data supplied.
	**
	** @return array of Models
	*/
	public function find($criteria=NULL,$orderBy=NULL,$limit=NULL,$columns=NULL,$returnArray=false){
		$result=false;
		$this->dbConnect($this->connection);
		
		//Handle WHERE criterias
		$where=($criteria===NULL)?"":"WHERE $criteria";
		
		//Handle orderBy
		$orderBy="ORDER BY ".(($orderBy===NULL)?"dateUpdated DESC":$orderBy);
		
		//Handle Limits
		$limit=($limit===NULL)?"":"LIMIT $limit";
		
		//Handle Columns
		$columns=($columns===NULL)?"*":$columns;
		
		$joins="";
		if(count($this->loadChildren)){
			for($c=0;$c<count($this->loadChildren);$c++){
				$joins.="LEFT JOIN `".$this->loadChildren[$c]."` ON `".$this->modelName."`.`".$this->loadChildren[$c]."_id`=`".$this->loadChildren[$c]."`.id
				";
			}
		}
		
		$sQl="
			SELECT
				$columns
			FROM
				`".$this->modelName."`
				$joins
			$where
			$orderBy
			$limit
		";
		//true=saveResultArray.
		if($this->db->exec($sQl,$returnArray)){
			$result=array();
			if($returnArray){
				$result=$this->db->rows;
			}else{
				while(!$this->db->eof){
					$row=$this->db->row;
					$keys=array_keys($row);
					if(class_exists($this->modelName)){
						$resultModel=new $this->modelName;
					}else{
						$resultModel=new model($this->modelName);
					}
					$gotId=false;
					foreach($keys as $key){
						if(is_string($key)){
							if($key=="id"&&!$gotId){
								$gotId=true;
								$resultModel->set($key,stripslashes($row[$key]));
							}elseif($key!="id"){
								$resultModel->set($key,stripslashes($row[$key]));
							}							
						}
					}
					#$resultModel->modelName=$this->modelName;
					$result[]=$resultModel;
					$this->db->moveNext();
				}
			}
		}else{
			#$this->db->debugErrors();
			$this->mergeErrors($this->db);
			$result=false;
		}
		$this->dbClose();
		return($result);
	}

	/*
	** Returns a array of records that are paged.
	**
	** @return array()
	**/
	public function getPagedModels($sQl=null,$page=1,$recordsPerPage=null){
		//Use config's default Records Per Page
		$recordsPerPage=(is_null($recordsPerPage))?$this->config->recordsPerPage:$recordsPerPage;
		
		// get an instance
		$model=$this->instance();
		
		// connect to the DB
		$model->dbConnect($this->connection);
		
		if(is_null($sQl)){
			$sQl="SELECT * FROM ".$this->modelName." WHERE status=1";
		}else{
			$sQl=ereg_replace(";","",$sQl);
			if(strpos(strtolower($sQl),"limit")>-1){
				$sQl=substr($sQl,0,strpos(strtolower($sQl),"limit"));
			}
		}
		
		// query for a general row count
		$countQuery="SELECT COUNT(*) rowCount ".substr($sQl,strpos(strtolower($sQl),"from"));
		$countQuery=substr($countQuery,0,strpos(strtolower($countQuery),"order by"));
		$result=$model->db->exec($countQuery,true);
		$rowCount=0;
		if($result){
			$rowCount=(int)$model->db->row["rowCount"];
		}else{
			$this->mergeErrors($model->db);
			#$this->debug("Number of errors",$this->errors);
		}
		
		$page=(int)$page;
		$pages=ceil($rowCount/$recordsPerPage);
		
		if($page>$pages){
			$page=$pages;
		}
		
		if($page<1){
			$page=1;
		}
		
		$limit="LIMIT ".(($page-1)*$recordsPerPage).",$recordsPerPage";
		
		// query for all the results
		$result=$model->db->exec("$sQl $limit",true);
		if($result){
			$models=$model->db->rows;
		}else{
			$this->mergeErrors($model->db);
			#$this->debug("Number of errors",$this->errors);
			$models=false;
		}
		
		// close the db connection
		$model->dbClose();
		
		$this->page=$page;
		$this->pages=$pages;
		$this->rowCount=$rowCount;
		$this->recordsPerPage=$recordsPerPage;
		
		return($models);
	}

	/*
	** Returns the current page number.
	**
	** @return int
	**/
	public function getPageNumber(){
		#return((isset($this->rv["page"]))?$this->rv["page"]:1);
	}

	/*
	** Set the Validation Field.
	**
	** @return string(fieldName);
	**/
	public function setValidationField($fieldName,$displayName=NULL,$inputType="text"){
		$displayName=($displayName===NULL)?$fieldName:$displayName;

		#$this->debug("Model Name:".$this->modelName);

		$this->validationRules[$this->modelName."_".$fieldName]=array(
			"fieldName"=>$this->modelName."_".$fieldName,
			"displayName"=>$displayName,
			"inputType"=>$inputType,
			"validate"=>array()
		);
		return($this->modelName."_".$fieldName);
	}

	/*
	** Add a validation rule to the field.
	**
	** @return void;
	**/
	public function addValidationRules($fieldName,$rules=NULL){
		if($rules!==NULL){
			$this->validationRules[$fieldName]["validate"]=array_merge($this->validationRules[$fieldName]["validate"],$rules);
		}
		#$this->debug("yeah1",$this->validationRules[$fieldName]);
	}

	/*
	** Shuffle Number.
	**
	** @return string - numerical;
	**/
	public function num_encrypt($num=NULL,$dateAdded=NULL){
            $result=false;
            $num=($num===NULL)?$this->get("id"):$num;
            $dateAdded=($dateAdded===NULL)?$this->get("dateAdded"):$dateAdded;
            $num=substr($dateAdded,-6,3).$num.substr($dateAdded,-3);
            $num=encryption::snis_encrypt($num, $this->config->RC4_KEY);
            $result=encryption::checksum($num, true);
            return($result);
	}

	/*
	** Unshuffles Number.
	**
	** @return string - numerical;
	**/
	public function num_decrypt($num=NULL){
            $result=0;
            if(encryption::checksum($num)){
				$this->debug("num",$num);
				$this->debug("num",substr($num,0,-1));
                $result=encryption::snis_decrypt(substr($num,0,-1), $this->config->RC4_KEY);
                //$this->getModelFromId($result);
				$result=substr($result,3,-3);
            }
            return($result);
	}
};
?>